DBus 自省 XML 生成 Qt 代码

各种支持 DBus 的开发框架都能够通过 XML 自动生成代码,例如 Glib 的 gdbus-codegen 和 Qt 的 qdbusxml2cpp

通过 DBus 对象 org.freedesktop.DBus.Introspectable 接口下的 Introspect 方法可以自省 XML,这样就不需要手写了。

但是 d-feet、dbus-send 等工具会给返回值加上类型标注或者换行符导致需要人工修改。因此需要自己写一个脚本来自省 DBus。

使用示例:

1$ ./introspect.py -t system -n org.freedesktop.NetworkManager -p /org/freedesktop/NetworkManager
2$ ./introspect.py -t system -n org.freedesktop.NetworkManager -p /org/freedesktop/NetworkManager/Devices/5
3$ ls -1
4introspect.py  
5org.freedesktop.NetworkManager.Device.Statistics.xml  
6org.freedesktop.NetworkManager.Device.WifiP2P.xml  
7org.freedesktop.NetworkManager.Device.xml
8org.freedesktop.NetworkManager.xml
9$ qdbusxml2cpp -c NetworkManager -p NetworkManager org.freedesktop.NetworkManager.xml --no-namespaces
10$ qdbusxml2cpp -c Device -p Device org.freedesktop.NetworkManager.Device.xml --no-namespaces
11$ qdbusxml2cpp -c WifiP2P -p WifiP2P org.freedesktop.NetworkManager.Device.WifiP2P.xml --no-namespaces
12$ ls -1
13Device.cpp
14Device.h
15introspect.py
16NetworkManager.cpp
17NetworkManager.h
18org.freedesktop.NetworkManager.Device.Statistics.xml
19org.freedesktop.NetworkManager.Device.WifiP2P.xml
20org.freedesktop.NetworkManager.Device.xml
21org.freedesktop.NetworkManager.xml
22WifiP2P.cpp
23WifiP2P.h

使用 --no-namespaces 选项的原因是,qdbusxml2cpp 会把 DBus Name 的最后一项作为类名,而之前项作为命名空间名 即 org.freedesktop.NetworkManager 生成 ::org::freedesktop::NetworkManager 类。 而 org.freedesktop.NetworkManager.Device 生成的 ::org::freedesktop::NetworkManager::Device 类。 这两者会发生冲突。前者的 NetworkManager 是类名,而后者的是命名空间明。

验证生成的代码:

1#include <QDebug>
2#include "NetworkManager.h"
3#include "Device.h"
4#include "WifiP2P.h"
5
6int main(void)
7{
8    NetworkManager networkManager{"org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager", QDBusConnection::systemBus()};
9    auto devicePathes = networkManager.devices();
10    for (auto& devicePath : devicePathes)
11    {
12        Device device("org.freedesktop.NetworkManager", devicePath.path(), QDBusConnection::systemBus());
13        qDebug() << "check wifi p2p device" << devicePath.path();
14        if (device.deviceType() == 30)
15        {
16            qDebug() << "found wifi p2p device" << devicePath.path();
17            break;
18        }
19    }
20}
117:06:43: Starting /home/planc/miracast/build-wifi-p2p-unknown-Default/wifi-p2p...
2check wifi p2p device "/org/freedesktop/NetworkManager/Devices/1"
3check wifi p2p device "/org/freedesktop/NetworkManager/Devices/2"
4check wifi p2p device "/org/freedesktop/NetworkManager/Devices/5"
5found wifi p2p device "/org/freedesktop/NetworkManager/Devices/5"
617:06:43: /home/planc/miracast/build-wifi-p2p-unknown-Default/wifi-p2p exited with code 0

如果看不到日志打印可以参考 Qt 日志模块的使用 进行配置

1#! /usr/bin/env python3
2import dbus
3from dbus.proxies import ProxyObject
4import xml.dom.minidom as minidom
5from typing import Callable, List, Set, Dict
6from argparse import ArgumentParser, Namespace
7
8# 不需要的 Interface
9filter:Set[str] = {
10    "org.freedesktop.DBus.Introspectable",
11    "org.freedesktop.DBus.Peer",
12    "org.freedesktop.DBus.Properties",
13}
14
15class DBusTypeParser(object):
16    dbusQtContainerType:Dict[str,str] = {
17        "<array>": "QList",
18        "<struct>": "QVariant",
19        "<dict>": "QMap",
20    }
21
22    dbusQtType:Dict[str,str] = {
23        "y": "quint8",
24        "b": "bool",
25        "n": "qint16",
26        "q": "quint16",
27        "i": "qint32",
28        "u": "quint32",
29        "x": "qint64",
30        "t": "quint64",
31        "d": "double",
32        "h": "quint32",
33        "s": "QString",
34        "o": "QDBusObject",
35        "g": "QString",
36        "v": "QVariant",
37    }
38
39    def __init__(self) -> None:
40        # 状态
41        self.currentState:str = "<normal>"
42        self.stateStack:List[str] = []
43
44        # dict里的第几个参数
45        self.currentIndex:int = 0
46        self.indexStack:List[int] = []
47
48    def pushState(self, state:str) -> None:
49        if state == "<dict>":
50            self.pushIndex(self.currentIndex)
51            self.currentIndex = 0
52        self.stateStack.append(state)
53
54    def popState(self) -> str:
55        state:str = self.stateStack.pop()
56        if state == "<dict>":
57            self.currentIndex = self.popIndex()
58        return state
59
60    def pushIndex(self, index:int) -> None:
61        self.indexStack.append(index)
62
63    def popIndex(self) -> int:
64        return self.indexStack.pop()
65
66    def parse(self, signature:str) -> str:
67        self.currentState = "<normal>"
68        self.currentIndex = 0
69        qtype:str = ""
70        while len(signature) > 0:
71            ch:str = signature[0]
72            signature = signature[1:]
73
74            if ch in DBusTypeParser.dbusQtType:
75                if self.currentState == "<normal>":
76                    qtype += self.parseNormal(ch)
77                elif self.currentState == "<struct>":
78                    qtype += self.parseStruct(ch)
79                elif self.currentState == "<dict>":
80                    qtype += self.parseDict(ch)
81                elif self.currentState == "<array>":
82                    qtype += self.parseArray(ch)
83            elif ch == "a" and signature:
84                if self.currentState == "<dict>" and self.currentIndex == 0:
85                    qtype += DBusTypeParser.dbusQtContainerType['<dict>'] + "<"
86                self.pushState(self.currentState)
87                self.currentState = "<array>"
88            elif ch == "(":
89                if self.currentState == "<dict>" and self.currentIndex == 0:
90                    qtype += DBusTypeParser.dbusQtContainerType['<dict>'] + "<"
91                self.pushState(self.currentState)
92                self.currentState = "<struct>"
93            elif ch == "{":
94                # a{ 开启dict模式,之前为暂态的array模式,不push
95                self.currentState = "<dict>"
96            elif ch == ")":
97                qtype += self.parseStruct(ch)
98                if self.currentState == "<dict>" and self.currentIndex == 0:
99                    qtype += ", "
100                    self.currentIndex += 1
101            elif ch == "}":
102                self.currentState = self.popState()
103                qtype += self.parseDict(ch)
104                if self.currentState == "<dict>" and self.currentIndex == 0:
105                    qtype += ", "
106                    self.currentIndex += 1
107            else:
108                print(f"{ch}")
109                raise f"Unknown signature '{ch}'"
110        return qtype
111
112    def parseNormal(self, ch:str) -> str:
113        return DBusTypeParser.dbusQtType[ch]
114        
115    def parseArray(self, ch:str) -> str:
116        self.currentState = self.popState()
117        return f"{DBusTypeParser.dbusQtContainerType['<array>']}<{DBusTypeParser.dbusQtType[ch]}>"
118
119    def parseStruct(self, ch:str) -> str:
120        if ch != ")":
121            return ""
122
123        self.currentState = self.popState()
124        if self.currentState == "<struct>":
125            return ""
126
127        if self.currentState == "<normal>":
128            return DBusTypeParser.dbusQtContainerType["<struct>"]
129
130        if self.currentState == "<dict>":
131            return DBusTypeParser.dbusQtContainerType["<struct>"]
132
133        if self.currentState == "<array>":
134            self.currentState = self.popState()
135            return f"{DBusTypeParser.dbusQtContainerType['<array>']}<{DBusTypeParser.dbusQtContainerType['<struct>']}>"
136
137    def parseDict(self, ch:str) -> str:
138        if ch == "}":
139            return ">"
140
141        self.currentIndex += 1
142        if self.currentIndex == 1:
143            return DBusTypeParser.dbusQtContainerType['<dict>'] + "<" + DBusTypeParser.dbusQtType[ch] + ", "
144        else:
145            return DBusTypeParser.dbusQtType[ch]
146
147dbusTypeParser = DBusTypeParser()
148
149parser:ArgumentParser = ArgumentParser(description='DBus Introspect XML')
150parser.add_argument("-t", "--type", default="session", help="bus type, system or session")
151parser.add_argument("-n", "--name", help="bus namae")
152parser.add_argument("-p", "--path", help="object path")
153args:Namespace = parser.parse_args()
154
155bus:dbus.Bus = dbus.SystemBus() if args.type == "system" else dbus.SessionBus() 
156proxy:ProxyObject = bus.get_object(args.name, args.path)
157xmlString:str = proxy.Introspect(dbus_interface='org.freedesktop.DBus.Introspectable')
158
159root:minidom.Document = minidom.parseString(xmlString)
160interfaces:List[minidom.Element] = root.getElementsByTagName("interface")
161
162neededInterfaces:List[minidom.Element] = []
163for interface in interfaces:
164    name:str = interface.getAttribute("name")
165    if name in filter:
166        continue
167
168    methods:List[minidom.Element] = interface.getElementsByTagName("method")
169    for method in methods:
170        inIndex:int = 0
171        outIndex:int = 0
172        methodArgs:List[minidom.Element] = method.getElementsByTagName("arg")
173        for arg in methodArgs:
174            sign:str = arg.getAttribute("type")
175            qtype:str = dbusTypeParser.parse(sign)
176            annotation:minidom.Element = root.createElement("annotation")
177            
178            if arg.getAttribute("direction") == "in":
179                annotation.setAttribute("name", f"org.qtproject.QtDBus.QtTypeName.In{inIndex}")
180                inIndex += 1
181            if arg.getAttribute("direction") == "out":
182                annotation.setAttribute("name", f"org.qtproject.QtDBus.QtTypeName.Out{outIndex}")
183                outIndex += 1
184
185            annotation.setAttribute("value", qtype)
186            method.appendChild(annotation)
187
188    properties = interface.getElementsByTagName("property")
189    for property in properties:
190        sign:str = property.getAttribute("type")
191        qtype:str = dbusTypeParser.parse(sign)
192        annotation:minidom.Element = root.createElement("annotation")
193        annotation.setAttribute("name", "org.qtproject.QtDBus.QtTypeName")
194        annotation.setAttribute("value", qtype)
195        property.appendChild(annotation)
196    
197    neededInterfaces.append(interface)
198
199
200for interface in neededInterfaces:
201    with open(interface.getAttribute("name") + ".xml", "w") as fp:
202        fp.write(interface.toprettyxml())